#!/usr/bin/env python2.5

# Freecell4Maemo, Copyright 2008, Roy Wood

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


# To Do:
# - on-screen toggle for smart move mode
# - intelligent use of empty stacks when moving columns
# - smart-move a column?
# - save game state

# - smart-mode, just set targetStack and fall through?


"""	
Freecell4Maemo is an implementation of the classic Freecell cardgame for the Nokia "Maemo" platform.
	
The code is pretty small, and I have tried to comment it effectively throughout, so you should have be able to
figure things out pretty easily.

Some of the more significant pieces are as follows:
	
class Rect - a rectangle; important members are top, left, width, height
class Card - a playing card; important members are cardnum(0-51), screen location/size, pixbuf
class CardStack - a stack of Card objects; important members are screen location/size, cards, "empty stack" pixbuf, stack suit
class Freecell - the main class for the app; uses the other classes as needed


Some significant points about the main "Freecell" class are:

- the __init__ method creates all the basic object members, loads all the card images, creates the GUI
- the GUI is a single window containing a GTK DrawingArea
- all drawing is done in an offscreen PixMap
- the offscreen PixMap is blitted to the DrawingArea in response to expose events
- the offscreen PixMap is created in the configure event handler, not __init__
- all GraphicContext objects are created in the configure event handler
- the configure handler also triggers a call to set the rects of the CardStacks (important for switching between fullscreen and smallscreen)
- the real game logic is in the button_press_event handler (and yes, it gets a little messy)

"""


ABOUT_TEXT = """    
    Freecell for Maemo

    (c) 2008 Roy Wood

    roy.wood@gmail.com

  http://code.google.com/p/freecell4maemo/


This game is an implementation of 
the classic Freecell card game for 
the Nokia Maemo platform.

To move a card, click once to select 
the card, then click in the 
destination location.

Click the return key (square button 
in the center of the directional 
keypad) to auto-move cards to the
ace stacks.

Click the escape key (swoopy arrow 
keypad button) to undo a move.


This program is free software: you 
can redistribute it and/or modify 
it under the terms of the GNU 
General Public License as published 
by the Free Software Foundation, 
either version 3 of the License, or 
(at your option) any later version.

This program is distributed in the 
hope that it will be useful, but 
WITHOUT ANY WARRANTY; without 
even the implied warranty of 
MERCHANTABILITY or FITNESS FOR A 
PARTICULAR PURPOSE.  See the GNU 
General Public License for more 
details.

You should have received a copy of 
the GNU General Public License 
along with this program.  If not, 
a copy may be found here: 
 
 <http://www.gnu.org/licenses/>

"""

import gtk
import pygtk
import time 
import random
import logging
import math


try:
	import hildon
	import osso
	
	osso_c = osso.Context("com.nokia.freecell4maemo", "1.0.0", False)
	
	hildonMode = True
	
except:
	hildonMode = False
	

# Size of the inset border for the window
FULLSCREEN_BORDER_WIDTH = 10
SMALLSCREEN_BORDER_WIDTH = 2

# Border between upper/lower sets of cards
VERT_SEPARATOR_WIDTH = 10


# Suit IDs
CLUBS = 0
DIAMONDS = 1
SPADES = 2
HEARTS = 3

SUITNAMES = [ "Clubs", "Diamonds", "Spades", "Hearts" ]

# Suit colours
BLACK = 0
RED = 1

# Number of cards per suit
CARDS_PER_SUIT = 13

# Card pixbufs 0-51 are the regular cards, 
NUMCARDS = 52

# Cards 52-55 are the suit-back cards (aces in top right of screen)
CLUBS_BACK = 52
DIAMONDS_BACK = 53
SPADES_BACK = 54
HEARTS_BACK = 55

# Card 56 is the the blank-back card (used to draw stacks with no cards)
BLANK_BACK = 56

# Card 57 is the fancy-back card (not currently used)
FANCY_BACK = 57

# Total number of card images
TOTALNUMCARDS = FANCY_BACK

# Number of card columns
NUMCOLUMNS = 8

# Number of "free cells"
NUMFREECELLS = 4

# Number of ace cards
NUMACES = 4

# Types of cards
FREECELL_TYPE = 0
ACE_TYPE = 1
REGULAR_TYPE = 2

# Folder containing the card images
CARDFOLDER = "/usr/share/freecell4maemo/card_images"

# Response constants for the "Move card or column" dialog (OK/CANCEL are the constants that the hildon.Note dialog returns)
MOVE_CARD_ID = gtk.RESPONSE_CANCEL
MOVE_COLUMN_ID = gtk.RESPONSE_OK


class Rect(object):
	# A basic rectangle object
	
	def __init__(self, left = 0, top = 0, width = 0, height = 0):
		self.left = int(left)
		self.top = int(top)
		self.width = int(width)
		self.height = int(height)
	
	def setRect(self, left = 0, top = 0, width = 0, height = 0):
		self.left = int(left)
		self.top = int(top)
		self.width = int(width)
		self.height = int(height)
	
	def enclosesXY(self, x, y):
		# Determine if a point lies within the Rect
		return ((x >= self.left) and (x < self.left + self.width) and (y >= self.top) and (y < self.top + self.height))
	
	def getLeftTop(self):
		return (self.left, self.top)
	
	def getLeftTopWidthHeight(self):
		return (self.left, self.top, self.width, self.height)
	
	def unionWith(self, otherRect):
		# Modify the Rect to include another Rect
		left = min(self.left, otherRect.left)
		right = max(self.left + self.width, otherRect.left + otherRect.width)
		top = min(self.top, otherRect.top)
		bottom = max(self.top + self.height, otherRect.top + otherRect.height)
		
		self.left = left
		self.top = top
		self.width = (right - left)
		self.height = (bottom - top)
	
	
	
class Card(object):
	# A Card object defined by card number (0-51), screen location and size, and pixbuf
	# Note that the cards are ordered A,2,3,4,5,6,7,8,9,10,J,Q,K
	# The suits are ordered Clubs, Diamonds, Hearts, Spades
	
	def __init__(self, cardNum, left = 0, top = 0, width = 0, height = 0, pixBuf = None):
		self.cardNum = cardNum
		self.rect = Rect(left, top, width, height)
		self.pixBuf = pixBuf
	
	def getSuit(self):
		return self.cardNum // CARDS_PER_SUIT
	
	def getSuitColour(self):
		return (self.cardNum // CARDS_PER_SUIT) % 2
	
	def getValue(self):
		return self.cardNum % CARDS_PER_SUIT
	
	def setRect(self, left = 0, top = 0, width = 0, height = 0):
		self.rect.setRect(left, top, width, height)
	
	def enclosesXY(self, x, y):
		# Determine if a point lies within the Card
		return self.rect.enclosesXY(x, y)
	
	def drawCard(self, drawable, gc, xyPt = None):
		# Draw the Card in the given drawable, using the supplied GC
		if (xyPt != None):
			left, top = xyPt
		else:
			left, top = self.rect.getLeftTop()
		
		drawable.draw_pixbuf(gc, self.pixBuf, 0, 0, left, top)
	
	def getLeftTop(self):
		return self.rect.getLeftTop()
	
	def getLeftTopWidthHeight(self):
		return self.rect.getLeftTopWidthHeight()
	
	def getRect(self):
		left, top, w, h = self.rect.getLeftTopWidthHeight()
		return Rect(left, top, w, h)


class CardStack(object):
	# An object representing a stack of cards
	# The CardStack contains a list of Card objects, possesses an onscreen location
	# The CardStack can draw itself; if there are no Cards, then the emptyStackPixBuf is displayed
	# The CardStack's yOffset controls the vertical offset of cards in the stack
	
	def __init__(self, left, top, emptyStackPixBuf, stackSuit, yOffset = 0):
		self.left = int(left)
		self.top = int(top)
		self.emptyStackPixBuf = emptyStackPixBuf
		self.yOffset = yOffset
		self.cardWidth = emptyStackPixBuf.get_width()
		self.cardHeight = emptyStackPixBuf.get_height()
		self.rect = Rect(self.left, self.top, self.cardWidth, self.cardHeight)
		self.stackSuit = stackSuit
		self.cards = [ ]
	
	def getNumCards(self):
		return len(self.cards)
	
	def clearStack(self):
		self.cards = [ ]
	
	def getRect(self):
		left, top, w, h = self.rect.getLeftTopWidthHeight()
		return Rect(left, top, w, h)
	
	def getLeftTopWidthHeight(self):
		return self.rect.getLeftTopWidthHeight()
	
	def setLeftTop(self, left, top):
		self.left = left
		self.top = top
		self.rect = Rect(self.left, self.top, self.cardWidth, self.cardHeight + self.yOffset * len(self.cards))
		for i in range(len(self.cards)):
			self.cards[i].setRect(self.left, self.top + self.yOffset * i, self.cardWidth, self.cardHeight)
	
	def pushCard(self, card):
		card.setRect(self.left, self.top + self.yOffset * len(self.cards), self.cardWidth, self.cardHeight)
		self.cards.append(card)
		self.rect = Rect(self.left, self.top, self.cardWidth, self.cardHeight + self.yOffset * len(self.cards))
	
	def getCardValueSuitColour(self, cardIndex):
		# Get the card value, suit, and colour of a card on the CardStack; negative cardIndex values work the expected way (e.g. -1 is last/top card); if a bad index value is supplied, return the stack suit (i.e. ace stack suit)
		if (cardIndex >= len(self.cards) or abs(cardIndex) > len(self.cards)):
			return -1, self.stackSuit, self.stackSuit % 2
		else:
			card = self.cards[cardIndex]
			return card.getValue(), card.getSuit(), card.getSuitColour()
	
	def getTopCardRect(self):
		# Get the rect of top card on the CardStack; return bare rect if there are no cards
		if (len(self.cards) > 0):
			return self.cards[-1].getRect()
		else:
			left, top, w, h = self.rect.getLeftTopWidthHeight()
			return Rect(left, top, w, h)
	
	def getNextTopCardLeftTop(self):
		# Get the top/left of the next card location on the stack (useful for animation)
		return (self.left, self.top + self.yOffset * len(self.cards))
	
	def popCard(self):
		# Remove the top card on the CardStack; return the popped Card or None
		if (len(self.cards) > 0):
			card = self.cards[-1]
			del self.cards[-1]
			self.rect.setRect(self.left, self.top, self.cardWidth, self.cardHeight + self.yOffset * len(self.cards))
			return card
		else:
			return None
	
	def enclosesXY(self, x, y):
		# Determine if a point lies within the CardStack
		return self.rect.enclosesXY(x, y)
	
	def drawStack(self, drawable, gc):
		# Draw the stack (or the "empty stack" image) in the given drawable, using the supplied GC
		if (len(self.cards) <= 0):
			left, top = self.rect.getLeftTop()
			drawable.draw_pixbuf(gc, self.emptyStackPixBuf, 0, 0, left, top)
		elif (self.yOffset == 0):
			self.cards[-1].drawCard(drawable, gc)
		else:
			for c in self.cards:
				c.drawCard(drawable, gc)
	
	def drawTopCard(self, drawable, gc):
		# Draw the top card (or the "empty stack" image) in the given drawable, using the supplied GC
		if (len(self.cards) <= 0):
			left, top = self.rect.getLeftTop()
			drawable.draw_pixbuf(gc, self.emptyStackPixBuf, 0, 0, left, top)
		else:
			self.cards[-1].drawCard(drawable, gc)
		

class FreeCell(object):
	# The real application....
	
	def __init__(self):
		# Init the rendering objects to None for now; they will be properly populated during the expose_event handling
		self.offscreenPixmap = None
		self.offscreenGC = None
		self.greenColour = None
		self.redColour = None
		self.blackColour = None
		self.whiteColour = None
		self.tmpPixmap = None
		self.tmpGC = None
		
		# Load the cards
		self.cardPixbufs = [ gtk.gdk.pixbuf_new_from_file("%s/%02d.gif" % (CARDFOLDER, i)) for i in range(TOTALNUMCARDS) ]
		
		# Load the "smart mode" image
		self.smartModePixbuf = gtk.gdk.pixbuf_new_from_file("%s/lightning.gif" % (CARDFOLDER))
		self.smartModeRect = Rect()
		
		# All cards are supposed to be the same height and width
		self.cardHeight = self.cardPixbufs[0].get_height()
		self.cardWidth = self.cardPixbufs[0].get_width()
		
		
		# Each group of cards (freecells, aces, columns) is stored in a list of CardStacks
		# We also keep track of a bounding rect for each group and use this rect when doing hit-testing of mouse clicks
		
		# Set up the "free cells" (4 cells in top left of screen)
		self.freecellStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, 0) for i in range(NUMFREECELLS) ]
		self.freeCellsRect = None
		
		# Set up the "aces" (4 cells in top right of screen); order is important!
		self.acesStacks = [ CardStack(0, 0, self.cardPixbufs[CLUBS_BACK + i], i, 0) for i in range(NUMACES) ]
		self.acesRect = None
		
		# Set up the columns
		self.mainCardStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, self.cardHeight // 5) for i in range(NUMCOLUMNS) ]
		self.mainCardsRects = None
		
		# Keep track of all card stack moves so we can undo moves
		self.undoStack = [ ]
		
		# Initialize the cards
		self.startCardOrder = []
		self.setupCards()
		
		# Default to manual play mode
		self.smartPlayMode = False
		
		
		# These get set properly during the configure event handler
		self.windowWidth = 0
		self.windowHeight = 0
		self.windowFullscreen = False
		
		
		# Create menus
		self.menu = gtk.Menu()
		
		menuItem = gtk.MenuItem("_New Game")
		menuItem.connect("activate", self.new_game_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
		menuItem = gtk.MenuItem("_Restart Game")
		menuItem.connect("activate", self.restart_game_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
		menuItem = gtk.MenuItem("_About...")
		menuItem.connect("activate", self.about_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
		menuItem = gtk.MenuItem("E_xit")
		menuItem.connect("activate", self.exit_menu_cb)
		self.menu.append(menuItem)
		menuItem.show()
		
		
		# Main part of window is a DrawingArea
		self.drawingArea = gtk.DrawingArea()
		
		
		global hildonMode
		
		if (hildonMode):
			# Main window contains a single DrawingArea; menu is attached to Hildon window
			self.app = hildon.Program()
			self.mainWindow = hildon.Window()
			self.mainWindow.set_title("Freecell")
			self.app.add_window(self.mainWindow)
			self.mainWindow.add(self.drawingArea)
			self.mainWindow.set_menu(self.menu)
			
			# Hildon dialogs are different than regular Gtk dialogs
			self.cardOrColumnDialog = hildon.Note("confirmation", (self.mainWindow, "Move column or card?", gtk.STOCK_DIALOG_QUESTION))
			self.cardOrColumnDialog.set_button_texts ("Column", "Card")
			
			self.youWinDialog = hildon.Note("information", (self.mainWindow, "You won!", gtk.STOCK_DIALOG_INFO))
			
			
		else:
			# Main window contains a VBox with a MenuBar and a DrawingArea
			self.mainWindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
			#self.mainWindow.set_default_size(800,600)
			self.drawingArea.set_size_request(800, 480)
			
			fileMenu = gtk.MenuItem("_File")
			fileMenu.set_submenu(self.menu)
			menuBar = gtk.MenuBar()
			menuBar.append(fileMenu)
			vbox = gtk.VBox()
			vbox.pack_start(menuBar, False, False, 2)
			vbox.pack_end(self.drawingArea, True, True, 2)
			self.mainWindow.add(vbox)
			
			# Create the dialogs in advance and then reuse later
			self.cardOrColumnDialog = gtk.Dialog(parent = self.mainWindow, flags = gtk.DIALOG_MODAL, buttons=("Column", MOVE_COLUMN_ID, "Card", MOVE_CARD_ID))
			self.cardOrColumnLabel = gtk.Label("Move column or card?")
			self.cardOrColumnDialog.vbox.pack_start(self.cardOrColumnLabel)
			self.cardOrColumnLabel.show()
			
			self.youWinDialog = gtk.MessageDialog(self.mainWindow, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, "You won!")
		
		
		# Wire up the event callbacks
		self.mainWindow.connect("delete_event", self.delete_event_cb)
		self.mainWindow.connect("destroy", self.destroy_cb)
		self.mainWindow.connect("key-press-event", self.key_press_cb)
		self.mainWindow.connect("window-state-event", self.window_state_change_cb)
		self.drawingArea.connect("expose_event", self.expose_event_cb)
		self.drawingArea.connect("configure_event", self.configure_event_cb)
		self.drawingArea.connect("button_press_event", self.button_press_event_cb)
		self.drawingArea.set_events(gtk.gdk.EXPOSURE_MASK | gtk.gdk.BUTTON_PRESS_MASK)
		
		# Create the "About" dialog
		self.aboutDialog = gtk.Dialog(parent = self.mainWindow, flags = gtk.DIALOG_MODAL, buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
		#self.aboutDialog.set_geometry_hints(self.mainWindow, min_width=400, min_height=200)
		self.aboutDialog.set_default_size(480,300)
		self.aboutScrolledWin = gtk.ScrolledWindow()
		self.aboutScrolledWin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
		self.aboutTextView = gtk.TextView()
		self.aboutTextView.set_editable(False)
		self.aboutTextView.get_buffer().set_text(ABOUT_TEXT)
		self.aboutScrolledWin.add(self.aboutTextView)
		self.aboutDialog.vbox.pack_start(self.aboutScrolledWin)
		
		# Behold!
		self.mainWindow.show_all()
		
		# Track the currently selected card
		self.selectedCardRect = Rect()
		self.selectedCardStack = None
		self.selectedCardType = None
		
		self.debugMode = False
	
	
	def exit_menu_cb(self, widget):
		gtk.main_quit()
	
	
	def restart_game_menu_cb(self, widget):
		self.setupCards(False)
		self.setCardRects()
		self.redrawOffscreen()
		self.updateRect(None)
	
	
	def about_menu_cb(self, widget):
		self.aboutDialog.show_all()
		self.aboutDialog.run()
		self.aboutDialog.hide()
	
	
	def new_game_menu_cb(self, widget):
		self.setupCards()
		self.setCardRects()
		self.redrawOffscreen()
		self.updateRect(None)
	
	
	def key_press_cb(self, widget, event, *args):
		if (event.keyval == gtk.keysyms.F6):
			if (self.windowFullscreen):
				self.mainWindow.unfullscreen()
			else:
				self.mainWindow.fullscreen()
		
		elif (event.keyval == gtk.keysyms.Up):
			print "Up!"
			self.smartPlayMode = False
			self.redrawSmartModeIcon()
			self.updateRect(self.smartModeRect)
		
		elif (event.keyval == gtk.keysyms.Down):
			print "Down!"
			self.smartPlayMode = True
			self.redrawSmartModeIcon()
			self.updateRect(self.smartModeRect)
		
		elif (event.keyval == gtk.keysyms.Left):
			print "Left!"
		
		elif (event.keyval == gtk.keysyms.Right):
			print "Right!"
			
		elif (event.keyval == gtk.keysyms.Escape):
			print "Escape!"
			self.undoMove()
		
		elif (event.keyval == gtk.keysyms.Return):
			print "Return!"
			self.autoMoveCardsHome()
		
		elif (event.keyval == gtk.keysyms.F7):
			print "Zoom +!"
			self.debugMode = False
		
		elif (event.keyval == gtk.keysyms.F8):
			print "Zoom -!"
			self.debugMode = True
	
	
	def autoMoveCardsHome(self):
		# Move cards to the ace stacks, where possible
		
		cardStacks = self.freecellStacks + self.mainCardStacks
		
		while (True):
			movedACard = False
			
			for srcStack in cardStacks:
				srcCardValue, srcCardSuit, srcCardSuitColour = srcStack.getCardValueSuitColour(-1)
				if (srcCardSuit >= 0):
					aceCardValue, aceCardSuit, aceCardSuitColour = self.acesStacks[srcCardSuit].getCardValueSuitColour(-1)
					if (srcCardValue == aceCardValue + 1):
						tempRect = srcStack.getTopCardRect()
						self.flashRect(tempRect)
						self.moveCard(srcStack, self.acesStacks[srcCardSuit])
						movedACard = True
			
			if (movedACard != True):
				break
	
	
	def checkGameOver(self):
		# Game over?
		numFullAceStacks = 0
		
		for stack in self.acesStacks:
			cardVal, cardSuit, cardColour = stack.getCardValueSuitColour(-1)
			if (cardVal == CARDS_PER_SUIT - 1):
				numFullAceStacks += 1
		
		if (numFullAceStacks == NUMACES):
			self.youWinDialog.show()
			self.youWinDialog.run()
			self.youWinDialog.hide()
	
	
	def undoMove(self):
		# Undo a move
		if (len(self.undoStack) > 0):
			srcStack, dstStack = self.undoStack[-1]
			self.moveCard(dstStack, srcStack)
			# The call to moveCard actually records the undo as a move, so we need to pop the last TWO entries in the stack
			del self.undoStack[-1]
			del self.undoStack[-1]
			self.clearCardSelection()
	
	
	def window_state_change_cb(self, widget, event, *args):
		# Handle a window state change to/from fullscreen
		logging.info("window_state_change_cb")
		if (event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN):
			self.windowFullscreen = True
		else:
			self.windowFullscreen = False
	
	
	def setupCards(self, doShuffle = True):
		# Shuffle deck, distribute cards into the columns
		
		self.undoStack = [ ]
		self.acesStacks = [ CardStack(0, 0, self.cardPixbufs[CLUBS_BACK + i], i, 0) for i in range(NUMACES) ]
		self.freecellStacks = [ CardStack(0, 0, self.cardPixbufs[BLANK_BACK], -1, 0) for i in range(NUMFREECELLS) ]
		
		if (doShuffle):
			cards = [i for i in range(NUMCARDS)]
			random.shuffle(cards)
			self.startCardOrder = cards
		else:
			cards = self.startCardOrder
		
		for i in range(NUMCOLUMNS):
			self.mainCardStacks[i].clearStack()
		
		for i in range(NUMCARDS):
			cardNum = cards[i]
			cardCol = i % NUMCOLUMNS
			newCard = Card(cardNum, pixBuf = self.cardPixbufs[cardNum])
			self.mainCardStacks[cardCol].pushCard(newCard)
	
	
	def getStackListEnclosingRect(self, cardStackList):
		# Get a rect that encloses all the cards in the given list of CardStacks
		
		rect = cardStackList[0].getRect()
		for i in range(1, len(cardStackList)):
			rect.unionWith(cardStackList[i].getRect())
		return rect
	
	
	def setCardRects(self):
		# Set the position of all card stacks; this is done in response to a configure event
		
		# Set location of main stacks of cards
		cardHorizSpacing = self.windowWidth / 8.0
		for i in range(NUMCOLUMNS):
			x = int(i * cardHorizSpacing + (cardHorizSpacing - self.cardWidth) // 2)
			self.mainCardStacks[i].setLeftTop(x, VERT_SEPARATOR_WIDTH + self.cardHeight + VERT_SEPARATOR_WIDTH)
		
		# Set location of free cells and aces
		cardHorizSpacing = self.windowWidth / 8.5
		for i in range(NUMFREECELLS):
			x = i * cardHorizSpacing + (cardHorizSpacing - self.cardWidth) // 2
			self.freecellStacks[i].setLeftTop(x, VERT_SEPARATOR_WIDTH)
			
			x = int((i + NUMFREECELLS + 0.5) * cardHorizSpacing + (cardHorizSpacing - self.cardWidth) // 2)
			self.acesStacks[i].setLeftTop(x, VERT_SEPARATOR_WIDTH)
		
		# Get the enclosing rects for click-testing
		self.mainCardsRects = self.getStackListEnclosingRect(self.acesStacks)
		self.freeCellsRect = self.getStackListEnclosingRect(self.freecellStacks)
		self.acesRect = self.getStackListEnclosingRect(self.acesStacks)
	
	
	def delete_event_cb(self, widget, event, data=None):
		# False means okay to delete
		return False
	
	
	def destroy_cb(self, widget, data=None):
		# Tell gtk to quit
		gtk.main_quit()
	
	
	def flashRect(self, rect, repeats = 3):
		# Flash/invert a rect onscreen
		
		if (rect == None):
			return
		
		for i in range(repeats):
			self.invertRect(rect)
			gtk.gdk.window_process_all_updates()
			time.sleep(0.125)
	
	
	def updateRect(self, rect):
		# Queue a redraw of an onscreen rect
		
		if (rect == None):
			x, y, w, h = 0, 0, self.windowWidth, self.windowHeight
		else:
			x, y, w, h = rect.getLeftTopWidthHeight()
		
		#logging.info("updateRect: (%d,%d) %dx%d" % (x, y, w + 1, h + 1))
		
		self.drawingArea.queue_draw_area(x, y, w + 1, h + 1)
	
	
	def invertRect(self, rect):
		# Invert a rect onscreen
		
		if (rect == None):
			return
		x, y, w, h = rect.getLeftTopWidthHeight()
		self.drawingAreaGC.set_foreground(self.whiteColour)
		self.drawingAreaGC.set_function(gtk.gdk.XOR)
		self.drawingArea.window.draw_rectangle(self.drawingAreaGC, True, x, y, w, h)
		self.drawingAreaGC.set_function(gtk.gdk.COPY)

	
	def redrawSmartModeIcon(self):
		# Redraw the "smart-mode" icon in the top/middle of the screen
		
		if (self.smartPlayMode):
			left, top, width, height = self.smartModeRect.getLeftTopWidthHeight()
			self.offscreenPixmap.draw_pixbuf(self.offscreenGC, self.smartModePixbuf, 0, 0, left, top)
		else:
			self.offscreenGC.set_foreground(self.greenColour)
			left, top, width, height = self.smartModeRect.getLeftTopWidthHeight()
			self.offscreenPixmap.draw_rectangle(self.offscreenGC, True, left, top, width, height)
		
	
	def redrawOffscreen(self):
		# Redraw the game board and all card stacks
		
		self.offscreenGC.set_foreground(self.greenColour)
		width, height = self.offscreenPixmap.get_size()
		self.offscreenPixmap.draw_rectangle(self.offscreenGC, True, 0, 0, width, height)
		
		for cardStack in self.acesStacks:
			cardStack.drawStack(self.offscreenPixmap, self.offscreenGC)
		
		for cardStack in self.freecellStacks:
			cardStack.drawStack(self.offscreenPixmap, self.offscreenGC)
		
		for cardStack in self.mainCardStacks:
			cardStack.drawStack(self.offscreenPixmap, self.offscreenGC)
		
		self.redrawSmartModeIcon()
		
	
	
	def configure_event_cb(self, widget, event):
		# Handle the window configuration event at startup or when changing to/from fullscreen
		
		logging.info("configure_event_cb")
		
		# Allocate a Pixbuf to serve as the offscreen buffer for drawing of the game board
		
		x, y, width, height = widget.get_allocation()
		self.offscreenPixmap = gtk.gdk.Pixmap(widget.window, width, height)
		self.offscreenGC = self.offscreenPixmap.new_gc()
		self.greenColour = self.offscreenGC.get_colormap().alloc_color(0x0000, 0x8000, 0x0000)
		self.redColour = self.offscreenGC.get_colormap().alloc_color(0xFFFF, 0x0000, 0x0000)
		self.blackColour = self.offscreenGC.get_colormap().alloc_color(0x0000, 0x0000, 0x0000)
		self.whiteColour = self.offscreenGC.get_colormap().alloc_color(0xFFFF, 0xFFFF, 0xFFFF)
		self.drawingAreaGC = self.drawingArea.window.new_gc()
		self.tmpPixmap = gtk.gdk.Pixmap(widget.window, width, height)
		self.tmpGC = self.tmpPixmap.new_gc()
		
		
		# Screen geometry has changed, so note new size, set CardStack locations, redraw screen
		self.windowWidth = width
		self.windowHeight = height
		
		logging.debug("configure_event_cb: self.windowWidth = %d, self.windowHeight = %d" % (self.windowWidth, self.windowHeight))
		
		# Resize has occurred, so set the card rects
		self.setCardRects()
		
		# Set the smart-mode icon rect
		left = (self.windowWidth - self.smartModePixbuf.get_width()) // 2
		top = 2 * VERT_SEPARATOR_WIDTH
		self.smartModeRect = Rect(left, top, self.smartModePixbuf.get_width(), self.smartModePixbuf.get_height())
		
		# Redraw everything
		self.redrawOffscreen()
		
		return True


	def expose_event_cb(self, widget, event):
		# Draw game board by copying from offscreen Pixbuf to onscreen window
		# Gtk is apparently now double-buffered, so this is probably unnecessary
		
		x , y, width, height = event.area
		
		logging.debug("expose_event_cb: x=%d, y=%d, w=%d, h=%d" % (x, y, width, height))
		
		if (self.offscreenPixmap != None):
			widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL], self.offscreenPixmap, x, y, x, y, width, height)
		
		return False
	
	
	def clearCardSelection(self):
		# Clear the current card selection (drawn inverted)
		
		#logging.info("clearCardSelection: (%d,%d) %dx%d" %(self.selectedCardRect.getLeftTopWidthHeight()))
		if (self.selectedCardRect != None):
			self.updateRect(self.selectedCardRect)
		self.selectedCardRect = None
		self.selectedCardType = None
		self.selectedCardStack = None
	
	
	def setCardSelection(self, stackType, cardStack, cardRect):
		# Set the selected/highlighted card
		
		self.invertRect(cardRect)
		self.selectedCardRect = cardRect
		self.selectedCardType = stackType
		self.selectedCardStack = cardStack
	
	
	def animateCardMove(self, card, toX,toY):
		# Cutesy animation showing movement of a card from its current location to a new location
		
		fromX,fromY,cardWidth,cardHeight = card.getLeftTopWidthHeight()
		
		if (fromX == toX and fromY == toY):
			return
		
		deltaX, deltaY = float(toX - fromX), float(toY - fromY)
		dist = math.sqrt(deltaX*deltaX + deltaY*deltaY)
		speed = 10.0
		numSteps = int(dist / speed)
		vx, vy = deltaX / numSteps, deltaY / numSteps
		updateWidth, updateHeight = cardWidth + int(abs(vx) + 0.5) + 1, cardHeight + int(abs(vy) + 0.5) + 1
		
		prevX, prevY = fromX, fromY
		
		for i in range(numSteps + 1):
			if (i == numSteps):
				# Avoid rounding issues
				x, y = int(toX), int(toY)
			else:
				x, y = int(fromX + vx * i), int(fromY + vy * i)
			
			left, top = min(x, prevX), min(y, prevY)
			
			self.tmpPixmap.draw_drawable(self.tmpGC, self.offscreenPixmap, left, top, left, top, updateWidth, updateHeight)
			card.drawCard(self.tmpPixmap, self.tmpGC, (x,y))
			self.drawingArea.window.draw_drawable(self.drawingArea.get_style().fg_gc[gtk.STATE_NORMAL], self.tmpPixmap, left, top, left, top, updateWidth, updateHeight)
			
			# Took me a long time to figure out that this forces screen updates and makes the animation work
			gtk.gdk.window_process_all_updates()
			
			prevX, prevY = x, y
			
			#time.sleep(0.1)
		
	
	def moveCard(self, srcStack, dstStack):
		# Move a card from one stack to another
		
		if (srcStack == dstStack):
			return
		
		srcCardVal, srcSuit, srcSuitColour = srcStack.getCardValueSuitColour(-1)
		dstCardVal, dstSuit, dstSuitColour = dstStack.getCardValueSuitColour(-1)
		
		logging.info("moveCard: move %s %d to %s %d" % (SUITNAMES[srcSuit], srcCardVal, SUITNAMES[dstSuit], dstCardVal))
		self.undoStack.append((srcStack, dstStack))
		
		x, y, w, h = srcStack.getTopCardRect().getLeftTopWidthHeight()
		self.offscreenGC.set_foreground(self.greenColour)
		self.offscreenPixmap.draw_rectangle(self.offscreenGC, True, x, y, w, h)
		
		fromX, fromY = x, y
		toX, toY = dstStack.getNextTopCardLeftTop()
		
		card = srcStack.popCard()
		srcStack.drawTopCard(self.offscreenPixmap, self.offscreenGC)
		
		self.animateCardMove(card, toX, toY)
		
		dstStack.pushCard(card)
		dstStack.drawTopCard(self.offscreenPixmap, self.offscreenGC)
		
		self.clearCardSelection()
		self.checkGameOver()
	
	
	def xyToCardStackInfo(self, x, y):
		# Determine the card/stack at a given (x,y); return the type, rect, cardStack of the target
		
		hitType = None
		hitRect = None
		hitStack = None
		
		if (self.freeCellsRect.enclosesXY(x, y)):
			for i in range(len(self.freecellStacks)):
				testHitStack = self.freecellStacks[i]
				if (testHitStack.enclosesXY(x, y)):
					hitStack = testHitStack
					hitRect = self.freecellStacks[i].getRect()
					hitType = FREECELL_TYPE
					break
		
		elif (self.acesRect.enclosesXY(x, y)):
			for i in range(len(self.acesStacks)):
				testHitStack = self.acesStacks[i]
				if (testHitStack.enclosesXY(x, y)):
					hitStack = testHitStack
					hitRect = self.acesStacks[i].getRect()
					hitType = ACE_TYPE
					break
		
		else:
			for i in range(len(self.mainCardStacks)):
				testHitStack = self.mainCardStacks[i]
				if (testHitStack.enclosesXY(x, y)):
					hitStack = testHitStack
					hitRect = self.mainCardStacks[i].getTopCardRect()
					hitType = REGULAR_TYPE
					break
		
		return (hitType, hitRect, hitStack)
	
	
	def button_press_event_cb(self, widget, event):
		# This is the big, ugly one-- all the gameplay rules are implemented here...
		
		x, y = event.x, event.y
		
		dstType, dstRect, dstStack = self.xyToCardStackInfo(x, y)
		
		if (dstStack == None or dstStack == self.selectedCardStack):
			# Didn't click on a valid target, so clear the previous click selection and bail
			self.clearCardSelection()
			return True
		
		if (self.selectedCardStack == None and dstStack.getNumCards() > 0 and not self.smartPlayMode):
			# There was no previous selection, and smart-mode is off, so select target and bail
			self.setCardSelection(dstType, dstStack, dstRect)
			return True
		
		if (self.selectedCardStack == None and dstStack.getNumCards() > 0 and self.smartPlayMode):
			# No previous selection, player has clicked a valid stack, and smart-mode is on; try to move the card to an ace stack, main stack, or free cell stack
			
			origDstType, origDstStack, origDstRect = dstType, dstStack, dstRect
			
			# Call it srcStack to make the code clearer
			srcStack = dstStack
			srcCardVal, srcCardSuit, srcCardSuitColour = srcStack.getCardValueSuitColour(-1)
			
			# Try the aces stack first
			dstStack = self.acesStacks[srcCardSuit]
			dstCardVal, dstCardSuit, dstCardSuitColour = dstStack.getCardValueSuitColour(-1)
			
			if (dstCardVal == srcCardVal - 1):
				self.moveCard(srcStack, dstStack)
				return True
			
			# Try a non-empty main stack
			for dstStack in self.mainCardStacks:
				dstCardVal, dstCardSuit, dstCardSuitColour = dstStack.getCardValueSuitColour(-1)
				if (dstCardVal >= 0 and dstCardVal == srcCardVal + 1 and dstCardSuitColour != srcCardSuitColour):
					self.moveCard(srcStack, dstStack)
					return True
			
			# Try an empty main stack or a freecell stack
			tmpStacks = self.mainCardStacks + self.freecellStacks
			for dstStack in tmpStacks:
				if (dstStack.getNumCards() <= 0):
					self.moveCard(srcStack, dstStack)
					return True
			
			# No joy, so just select the card and bail
			self.setCardSelection(dstType, origDstStack, dstRect)
			return True
		
		
		# From here on out, we need a selected card, so bail if there is none
		if (self.selectedCardStack == None):
			return True
		
		if (self.debugMode):
			# Debug mode allows us to move a card anywhere we like, bwahahahahaha....
			self.moveCard(self.selectedCardStack, dstStack)
			return True
		
		if (dstType == ACE_TYPE):
			# Ace stack destination is special, so handle that here
			srcStack = self.selectedCardStack
			srcCardVal, srcCardSuit, srcCardSuitColour = srcStack.getCardValueSuitColour(-1)
			dstCardVal, dstCardSuit, dstCardSuitColour = dstStack.getCardValueSuitColour(-1)
			
			if (srcCardSuit == dstCardSuit and srcCardVal == dstCardVal + 1):
				# Move selected card to an ace stack
				self.moveCard(srcStack, dstStack)
			
			return True
			
		
		if (dstType == FREECELL_TYPE):
			# Freecell stack destination is also special, so handle that here
			if (dstStack.getNumCards() <= 0):
				self.moveCard(self.selectedCardStack, dstStack)
			
			return True
			
		
		# This is where things get complicated as we figure out whether we are moving a single card or a run
		
		srcStack = self.selectedCardStack
		srcNumCards = srcStack.getNumCards()
		srcCardVal, srcSuit, srcSuitColour = srcStack.getCardValueSuitColour(-1)
		dstNumCards = dstStack.getNumCards()
		dstCardVal, dstCardSuit, dstCardSuitColour = dstStack.getCardValueSuitColour(-1)
		dstSrcDelta = dstCardVal - srcCardVal
		logging.debug("srcSuit = %d, srcSuitColour = %d, srcCardVal = %d, srcNumCards = %d" % (srcSuit, srcSuitColour, srcCardVal, srcNumCards))
		logging.debug("dstCardSuit = %d, dstCardSuitColour = %d, dstCardVal = %d" % (dstCardSuit, dstCardSuitColour, dstCardVal))
		
		numFreeCells = 0
		for cardStack in self.freecellStacks:
			if (cardStack.getNumCards() <= 0):
				numFreeCells += 1
		
		runLength = 1
		for i in range(1, srcNumCards):
			cardVal, cardSuit, cardSuitColour = srcStack.getCardValueSuitColour(srcNumCards - i - 1)
			logging.debug("card #%d: cardVal = %d, cardSuit = %d, cardSuitColour = %d" % (srcNumCards - i - 1, cardVal, cardSuit, cardSuitColour))
			if (cardVal == srcCardVal + i and cardSuitColour == (srcSuitColour + i) % 2):
				runLength += 1
			else:
				break
		
		suitColoursWork = (srcSuitColour == (dstCardSuitColour + dstSrcDelta) % 2)
		srcRunMeetsDst = dstSrcDelta > 0 and runLength >= dstSrcDelta
		
		logging.debug("dstSrcDelta = %d, numFreeCells = %d, runLength = %d, suitColoursWork = %s, srcRunMeetsDst = %s" % (dstSrcDelta, numFreeCells, runLength, suitColoursWork, str(srcRunMeetsDst)))
		
		if (dstNumCards <= 0 and runLength > 1 and numFreeCells > 0):
			# Move a card or a column to an empty stack?
			self.cardOrColumnDialog.show()
			dialogResult = self.cardOrColumnDialog.run()
			self.cardOrColumnDialog.hide()
			
			# Repaint the mess made by the dialog box
			self.updateRect(None)
			x, y, w, h = 0, 0, self.windowWidth, self.windowHeight
			self.drawingArea.window.draw_drawable(self.drawingArea.get_style().fg_gc[gtk.STATE_NORMAL], self.offscreenPixmap, x, y, x, y, w, h)
			gtk.gdk.window_process_all_updates()
			
			if (dialogResult == MOVE_CARD_ID):
				runLength = 1
		
		if (dstNumCards <= 0):
			# Move a run of cards onto an empty stack
			print "Move a run of cards onto an empty stack, numFreeCells=",numFreeCells," runLength=",runLength
			runLength = min(numFreeCells + 1, runLength)
			
		elif (srcRunMeetsDst and suitColoursWork and numFreeCells >= dstSrcDelta - 1):
			# Moving a run onto a non-empty stack
			runLength = dstSrcDelta
			
		else:
			# Move request is illegal, so assume user is just changing the active selection
			self.clearCardSelection()
			self.setCardSelection(dstType, dstStack, dstRect)
			return True
		
		logging.debug("Column move...")
		
		tempStacks = [ ]
		
		for i in range(runLength - 1):
			for j in range(NUMFREECELLS):
				if (self.freecellStacks[j].getNumCards() <= 0):
					self.moveCard(srcStack, self.freecellStacks[j])
					tempStacks.insert(0, self.freecellStacks[j])
					break
		
		self.moveCard(srcStack, dstStack)
		
		for s in tempStacks:
			self.moveCard(s, dstStack)
		
		
		return True

	
if __name__ == "__main__":
	logging.basicConfig(level=logging.INFO)
	
	freeCell = FreeCell()
	
	gtk.main()
